/*
 * Copyright (C) 2013 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package okhttp3;

import java.net.URL;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import okhttp3.internal.Util;
import okhttp3.internal.http.HttpMethod;

/**
 * An HTTP request. Instances of this class are immutable if their {@link #body}
 * is null or itself immutable.
 */
public final class Request {
	// 交易请求地址
	final HttpUrl url;
	// 请求方式，比如：get/post/http...等请求方式
	final String method;
	// 请求头
	final Headers headers;
	// 请求体
	final @Nullable RequestBody body;
	final Map<Class<?>, Object> tags;

	private volatile @Nullable CacheControl cacheControl; // Lazily initialized.

	Request(Builder builder) {
		this.url = builder.url;
		this.method = builder.method;
		this.headers = builder.headers.build();
		this.body = builder.body;
		this.tags = Util.immutableMap(builder.tags);
	}

	public HttpUrl url() {
		return url;
	}

	public String method() {
		return method;
	}

	public Headers headers() {
		return headers;
	}

	public @Nullable String header(String name) {
		return headers.get(name);
	}

	public List<String> headers(String name) {
		return headers.values(name);
	}

	public @Nullable RequestBody body() {
		return body;
	}

	/**
	 * Returns the tag attached with {@code Object.class} as a key, or null if no
	 * tag is attached with that key.
	 *
	 * <p>
	 * Prior to OkHttp 3.11, this method never returned null if no tag was attached.
	 * Instead it returned either this request, or the request upon which this
	 * request was derived with {@link #newBuilder()}.
	 */
	public @Nullable Object tag() {
		return tag(Object.class);
	}

	/**
	 * Returns the tag attached with {@code type} as a key, or null if no tag is
	 * attached with that key.
	 */
	public @Nullable <T> T tag(Class<? extends T> type) {
		return type.cast(tags.get(type));
	}

	public Builder newBuilder() {
		return new Builder(this);
	}

	/**
	 * Returns the cache control directives for this response. This is never null,
	 * even if this response contains no {@code Cache-Control} header.
	 */
	public CacheControl cacheControl() {
		CacheControl result = cacheControl;
		return result != null ? result : (cacheControl = CacheControl.parse(headers));
	}

	public boolean isHttps() {
		return url.isHttps();
	}

	@Override
	public String toString() {
		return "Request{method=" + method + ", url=" + url + ", tags=" + tags + '}';
	}

	public static class Builder {
		@Nullable
		HttpUrl url;
		String method;
		Headers.Builder headers;
		@Nullable
		RequestBody body;

		/** A mutable map of tags, or an immutable empty map if we don't have any. */
		Map<Class<?>, Object> tags = Collections.emptyMap();

		public Builder() {
			this.method = "GET";
			this.headers = new Headers.Builder();
		}

		Builder(Request request) {
			this.url = request.url;
			this.method = request.method;
			this.body = request.body;
			this.tags = request.tags.isEmpty() ? Collections.<Class<?>, Object>emptyMap()
					: new LinkedHashMap<>(request.tags);
			this.headers = request.headers.newBuilder();
		}

		public Builder url(HttpUrl url) {
			if (url == null)
				throw new NullPointerException("url == null");
			this.url = url;
			return this;
		}

		/**
		 * Sets the URL target of this request.
		 *
		 * @throws IllegalArgumentException if {@code url} is not a valid HTTP or HTTPS
		 *                                  URL. Avoid this exception by calling
		 *                                  {@link HttpUrl#parse}; it returns null for
		 *                                  invalid URLs.
		 */
		public Builder url(String url) {
			if (url == null)
				throw new NullPointerException("url == null");

			// Silently replace web socket URLs with HTTP URLs.
			if (url.regionMatches(true, 0, "ws:", 0, 3)) {
				url = "http:" + url.substring(3);
			} else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
				url = "https:" + url.substring(4);
			}

			return url(HttpUrl.get(url));
		}

		/**
		 * Sets the URL target of this request.
		 *
		 * @throws IllegalArgumentException if the scheme of {@code url} is not
		 *                                  {@code http} or {@code
		 * https}                        .
		 */
		public Builder url(URL url) {
			if (url == null)
				throw new NullPointerException("url == null");
			return url(HttpUrl.get(url.toString()));
		}

		/**
		 * Sets the header named {@code name} to {@code value}. If this request already
		 * has any headers with that name, they are all replaced.
		 */
		public Builder header(String name, String value) {
			headers.set(name, value);
			return this;
		}

		/**
		 * Adds a header with {@code name} and {@code value}. Prefer this method for
		 * multiply-valued headers like "Cookie".
		 *
		 * <p>
		 * Note that for some headers including {@code Content-Length} and
		 * {@code Content-Encoding}, OkHttp may replace {@code value} with a header
		 * derived from the request body.
		 */
		public Builder addHeader(String name, String value) {
			headers.add(name, value);
			return this;
		}

		/** Removes all headers named {@code name} on this builder. */
		public Builder removeHeader(String name) {
			headers.removeAll(name);
			return this;
		}

		/** Removes all headers on this builder and adds {@code headers}. */
		public Builder headers(Headers headers) {
			this.headers = headers.newBuilder();
			return this;
		}

		/**
		 * Sets this request's {@code Cache-Control} header, replacing any cache control
		 * headers already present. If {@code cacheControl} doesn't define any
		 * directives, this clears this request's cache-control headers.
		 */
		public Builder cacheControl(CacheControl cacheControl) {
			String value = cacheControl.toString();
			if (value.isEmpty())
				return removeHeader("Cache-Control");
			return header("Cache-Control", value);
		}

		public Builder get() {
			return method("GET", null);
		}

		public Builder head() {
			return method("HEAD", null);
		}

		public Builder post(RequestBody body) {
			return method("POST", body);
		}

		public Builder delete(@Nullable RequestBody body) {
			return method("DELETE", body);
		}

		public Builder delete() {
			return delete(Util.EMPTY_REQUEST);
		}

		public Builder put(RequestBody body) {
			return method("PUT", body);
		}

		public Builder patch(RequestBody body) {
			return method("PATCH", body);
		}

		public Builder method(String method, @Nullable RequestBody body) {
			if (method == null)
				throw new NullPointerException("method == null");
			if (method.length() == 0)
				throw new IllegalArgumentException("method.length() == 0");
			if (body != null && !HttpMethod.permitsRequestBody(method)) {
				throw new IllegalArgumentException("method " + method + " must not have a request body.");
			}
			if (body == null && HttpMethod.requiresRequestBody(method)) {
				throw new IllegalArgumentException("method " + method + " must have a request body.");
			}
			this.method = method;
			this.body = body;
			return this;
		}

		/** Attaches {@code tag} to the request using {@code Object.class} as a key. */
		public Builder tag(@Nullable Object tag) {
			return tag(Object.class, tag);
		}

		/**
		 * Attaches {@code tag} to the request using {@code type} as a key. Tags can be
		 * read from a request using {@link Request#tag}. Use null to remove any
		 * existing tag assigned for {@code
		 * type}.
		 *
		 * <p>
		 * Use this API to attach timing, debugging, or other application data to a
		 * request so that you may read it in interceptors, event listeners, or
		 * callbacks.
		 */
		public <T> Builder tag(Class<? super T> type, @Nullable T tag) {
			if (type == null)
				throw new NullPointerException("type == null");

			if (tag == null) {
				tags.remove(type);
			} else {
				if (tags.isEmpty())
					tags = new LinkedHashMap<>();
				tags.put(type, type.cast(tag));
			}

			return this;
		}

		public Request build() {
			if (url == null)
				throw new IllegalStateException("url == null");
			return new Request(this);
		}
	}
}
